﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using DevDefined.OAuth.Framework;
using Lightray.Hattrick.ChppLib.EntityTypes;

namespace Lightray.Hattrick.ChppLib
{
    // need to integate youthplayers to this and DataCache

    // needs to seprate itrick tasks from general tasks, ie. scheduling / sampling of chpp service should be somewhere else.
    // this class should only handle reading from storage / instancing / cross connecting objects 

    // need to read latest copies of each xml
    // for FileSystemStorage:        
    // can be achived by saving the latest copies in a different public entry for quick access insted of sorting files
    // and also a copy for the archived ones
    public class DataManager
    {
        private const string LATEST_DATA_STORAGE_ENTRY_NAME = "latestData";
        private FileSystemStorage<ITrickStorageEntry> _storage;
        private WebFetcher _fetcher;
        private DataCache _cache;
        private WebFetchingScheduler _scheduler;
        private Thread _updateDataThread;
        private Mutex _updateThreadMutex;
        private ClockThread _clockThread;
        private WebFetchingScheduler.DataTimes _latestDataTimes;
        private ITrickStorageEntry _latestData;
        private bool _countryWasUpdated;
        private volatile bool _dataMangerStarted;        

        public event EventHandler<DataManagerEventArgs> NewDataArrived;

        public Team MyTeam { get; private set; }
        
        // TODO: protect as single instance
        public DataManager(Guid storageId, string storagePath, IToken oauthToken, string chppConsumerSecret)
        {
            _updateThreadMutex = new Mutex();
            _countryWasUpdated = false;

            if (!Directory.Exists(storagePath))
                throw new ArgumentException("path does not exist", storagePath);

            _cache = new DataCache();

            _fetcher = new WebFetcher(oauthToken, chppConsumerSecret);
            _fetcher.FetchComplete += new EventHandler<FetchResultEventArgs>(onFetchComplete);

            _scheduler = new WebFetchingScheduler();

            _storage = new FileSystemStorage<ITrickStorageEntry>(storagePath, storageId);
            
            _clockThread = new ClockThread();
            _clockThread.Start();       
        }

        ~DataManager()
        {
            Stop();
        }


        public void StartDataManager()
        {
            startUpdateDataThread();
        }

        public void Stop()
        {
            _clockThread.Stop();
            _dataMangerStarted = false;
            if (_updateDataThread != null)
                _updateDataThread.Abort();
        }
        
        private void startUpdateDataThread()
        {
            _dataMangerStarted = true;
            if (_updateDataThread == null)
                new Thread(updateDataThread).Start();
        }

        private void updateDataThread()
        {
            _updateDataThread = Thread.CurrentThread;
            _updateDataThread.IsBackground = true;

            _clockThread.WaitForInitialClock(); // TOOD: what happans when no clock
            tryReadLatestDataFromStorage();

            while (_dataMangerStarted)
            {
                try
                {
                    _updateThreadMutex.WaitOne();
                    if (_fetcher._fetching)
                        continue;

                    Debug.Print("{0} {1} tick" ,_clockThread.CurrentTime, DateTime.Now);                    
                    updateFromWeb(_scheduler.GetSchedule(_clockThread.CurrentTime, _latestDataTimes, getUpcomingMatch()));                    
                }
                finally
                {
                    _updateThreadMutex.ReleaseMutex();
                    Thread.Sleep(TimeSpan.FromSeconds(60));
                }                
            }

            Debug.WriteLine("Update data thread stopped");
        }

        private void updateFromWeb(FetchRequest request)
        {
            Debug.WriteLine(request);
            if (request == FetchRequest.Nothing)
                return;

            // WHY it's inside DataManager, move to fetcher and have it spring an event that requestes the deatils for the fetch!
            // for now
            //if (isRequested(request, FetchRequest.MyLastMatchDeatils))
            //{
            //    Match lastMatch = getLastMatch();
            //    if (lastMatch != null)
            //        _fetcher.QueueMatchDetailsById(lastMatch.ID);
            //}

            //if (isRequested(request, FetchRequest.MyOpponentDetails))
            //{
            //    Match upcomingMatch = getUpcomingMatch();
            //    if (upcomingMatch != null)
            //        _fetcher.QueueTeamById(upcomingOpponentTeamId(upcomingMatch));
            //}

            _fetcher.Fetch(request);
        }

        private int upcomingOpponentTeamId(Match upcoming)
        {
            return upcoming.HomeTeamId == MyTeam.ID ? upcoming.AwayTeamId : upcoming.HomeTeamId;
        }

        private bool isRequested(FetchRequest fullRequest, FetchRequest requestSingleItem)
        {
            return (fullRequest & requestSingleItem) == requestSingleItem;
        }

        private void onFetchComplete(object sender, FetchResultEventArgs e)
        {
            updateClock(e);
            processNewData(e);
            storeLatestData(e);
            // TODO: store xmls
        }

        private void processNewData(FetchResultEventArgs e)
        {
            try
            {
                _updateThreadMutex.WaitOne();
                instanceEntites(e);
                connectEntityHierarchy(e);
                updateMyTeamData(e);
                updateLatestDataInformation(e);
                updateScheduler();                
                signalNewDataArrived(e);
            }
            finally
            {
                _updateThreadMutex.ReleaseMutex();
            }
        }

        private void signalNewDataArrived(FetchResultEventArgs e)
        {
            if (NewDataArrived == null)
                return;

            NewDataArrived(this,
                           new DataManagerEventArgs()
                           {
                               HattrickTime = e.HattrickTime,
                               MyTeam = MyTeam,
                               OffsetFromHattrickTime  = e.OffsetFromHattrickTime,
                           });
        }

        private void tryReadLatestDataFromStorage()
        {
            //TODO: better handling of storage related exceptions
            try
            {
                _storage.ReadEntry(LATEST_DATA_STORAGE_ENTRY_NAME, out _latestData);
            }
            catch (FileSystemStorageException)
            {
                return;
            }            

            List<XDocument> xmlsFromStorage = new List<XDocument>();
            xmlsFromStorage.AddRange(new XDocument[] { 
                                        _latestData.MyCountry,
                                        _latestData.MyEconomy,
                                        _latestData.MyLeague,
                                        _latestData.MyMatches,
                                        _latestData.MyPlayers,
                                        _latestData.MyRegion,
                                        _latestData.MyTeam,
                                        _latestData.MyLastMatchDeatils,
                                     });
            xmlsFromStorage.AddRange(_latestData.MyLeagueOtherTeams);

            processNewData(new FetchResultEventArgs()
                           {
                               HattrickTime = _clockThread.CurrentTime,
                               OffsetFromHattrickTime = _latestData.OffsetFromHattrickTime,
                               Xmls = xmlsFromStorage,
                               MyTeamId = _latestData.MyTeamId,
                               MyUserId = _latestData.MyUserId,
                               Request = _latestData.OriginalRequest,
                           });
        }

        private void storeLatestData(FetchResultEventArgs e)
        {
            _latestData.OriginalRequest = e.Request;
            _latestData.WrittenDate = _clockThread.CurrentTime;
            _latestData.OffsetFromHattrickTime = e.OffsetFromHattrickTime;
            _latestData.MyTeamId = e.MyTeamId > 0 ? e.MyTeamId : _latestData.MyTeamId;
            _latestData.MyUserId = e.MyUserId > 0 ? e.MyUserId : _latestData.MyUserId;
            _storage.Write(LATEST_DATA_STORAGE_ENTRY_NAME, _latestData);
        }

        private Match getLastMatch()
        {
            if (MyTeam == null || MyTeam.Matches == null)
                return null;

            return (from match in MyTeam.Matches
                    orderby match.MatchDate descending
                    where match.Status == MatchStatus.Finished
                    select match).FirstOrDefault();
        }

        private Match getUpcomingMatch()
        {
            if (MyTeam == null || MyTeam.Matches == null)
                return null;

            return (from match in MyTeam.Matches
                    orderby match.MatchDate
                    where (match.Status == MatchStatus.Upcoming  || match.Status  == MatchStatus.Ongoing) &&
                          !twelveHoursPassedSinceMatch(match)
                    select match).FirstOrDefault();
        }

        private bool twelveHoursPassedSinceMatch(Match match)
        {
            return (_clockThread.CurrentTime - match.MatchDate).TotalHours > 12;
        }        

        private void updateScheduler()
        {
            if (!_countryWasUpdated || MyTeam.Country == null)
                return;

            Debug.WriteLine("Updating scheduler with new league times");
            _scheduler.UpdateDownloadTimes(MyTeam.Country);
            _countryWasUpdated = false;
        }

        private void updateClock(FetchResultEventArgs e)
        {
            _clockThread.SetTime(e.HattrickTime);
        }
     
        private void updateMyTeamData(FetchResultEventArgs e)
        {
            if (MyTeam == null && e.MyTeamId > 0) // first time
            {
                MyTeam = _cache.TeamById(e.MyTeamId);
                return;
            }

            tryUpdateMyTeamExternals();
            tryUpdateMyTeamInstance(_cache.TeamById(MyTeam.ID));
        }

        private void tryUpdateMyTeamExternals()
        {
            LeagueCountry myCountry = _cache.CountryByLeagueID(MyTeam.LeagueID);
            LeagueLevelUnit myLeague = _cache.LeagueUnitByID(MyTeam.LeagueLevelUnitID);
            TeamLeagueStats myLeagueStats = _cache.LeagueStatsByTeamID(MyTeam.ID);
            EntityCollection<Player> myPlayers = _cache.PlayersByTeamID(MyTeam.ID);
            EntityCollection<Match> myMatches = _cache.MatchesByTeamId(MyTeam.ID);
            Region myRegion = _cache.RegionByRegionID(MyTeam.RegionID);

            MyTeam.Country = myCountry != null ? myCountry : MyTeam.Country;
            MyTeam.League = myLeague != null ? myLeague : MyTeam.League;
            MyTeam.LeagueStats = myLeagueStats != null ? myLeagueStats : MyTeam.LeagueStats;
            MyTeam.Players = myPlayers.Count > 0 ? myPlayers : MyTeam.Players;
            MyTeam.Matches = myMatches.Count > 0? myMatches : MyTeam.Matches;
            MyTeam.Region = myRegion != null ? myRegion : MyTeam.Region;
            MyTeam.Economy = _cache.MyEconomy != null ? _cache.MyEconomy : MyTeam.Economy;
        }

        private void tryUpdateMyTeamInstance(Team myUpdatedTeam)
        {
            if (myUpdatedTeam == null)
                return;
            Team olderTeam = MyTeam;
            MyTeam = myUpdatedTeam;

            MyTeam.Country = olderTeam.Country;
            MyTeam.League = olderTeam.League;
            MyTeam.LeagueStats = olderTeam.LeagueStats;
            MyTeam.Players = olderTeam.Players;
            MyTeam.Matches = olderTeam.Matches;
            MyTeam.Region = olderTeam.Region;
            MyTeam.Economy = olderTeam.Economy;            
        }

        private void updateLatestDataInformation(FetchResultEventArgs e)
        {
            List<XDocument> myLeagueOtherTeams = new List<XDocument>();

            foreach (XDocument xml in e.Xmls)
            {
                if (xml == null)
                    continue;

                HattrickXmlReader reader = new HattrickXmlReader(xml);
                string fileName = reader.ReadString("FileName").ToLower();
                DateTime fetchedDate = reader.ReadDate("FetchedDate");
                switch (fileName)
                {
                    case "teamdetails.xml":
                        if (reader.ReadNumber("TeamID") == MyTeam.ID)
                        {
                            _latestDataTimes.MyTeam = fetchedDate;
                            _latestData.MyTeam = xml;
                        }
                        else if (reader.ReadNumber("LeagueLevelUnitID") == MyTeam.LeagueLevelUnitID)                       
                            myLeagueOtherTeams.Add(xml);
                        break;

                    case "players.xml":
                        if (reader.ReadNumber("TeamID") == MyTeam.ID)
                        {
                            _latestDataTimes.MyPlayers = fetchedDate;
                            _latestData.MyPlayers = xml;
                        }
                        break;

                    case "worlddetails.xml":
                        _latestDataTimes.MyCountry = fetchedDate;
                        _latestData.MyCountry = xml;
                        break;

                    case "regiondetails.xml":
                        if (reader.ReadNumber("RegionID") == MyTeam.RegionID)
                        {
                            _latestDataTimes.MyRegion = fetchedDate;
                            _latestData.MyRegion = xml;
                        }
                        break;

                    case "leaguedetails.xml":
                    case "leaguefixtures.xml":
                        if (reader.ReadNumber("LeagueLevelUnitID") == MyTeam.LeagueLevelUnitID)
                        {
                            _latestDataTimes.MyLeagueAndTeams = fetchedDate;
                            _latestData.MyLeague = xml;
                        }
                        break;               

                    case "economy.xml":
                        _latestDataTimes.MyEconomy = fetchedDate;
                        _latestData.MyEconomy = xml;
                        break;

                    case "matches.xml":
                        if (reader.ReadNumber("TeamID") == MyTeam.ID)
                        {
                            _latestDataTimes.MyMatches = fetchedDate;
                            _latestData.MyMatches = xml;
                        }
                        break;
                    case "matchdetails.xml":
                        if (isMyLastMatch(reader, fetchedDate))
                        {
                            _latestDataTimes.MyLastMatch = fetchedDate;
                            _latestData.MyLastMatchDeatils = xml;
                        }
                        break;
                }
            }

            _latestData.MyLeagueOtherTeams = myLeagueOtherTeams.Count > 0 ? myLeagueOtherTeams : _latestData.MyLeagueOtherTeams;
        }

        private bool isMyLastMatch(HattrickXmlReader reader, DateTime fetchedDate)
        {
            return (reader.ReadNumber("HomeTeamID") == MyTeam.ID || reader.ReadNumber("AwayTeamID") == MyTeam.ID) &&
                    getLastMatch().ID == reader.ReadNumber("MatchID");
        }

        private void connectEntityHierarchy(FetchResultEventArgs e)
        {
            foreach (var team in _cache.Teams)
            {
                team.Players = _cache.PlayersByTeamID(team.ID);
                team.LeagueStats = _cache.LeagueStatsByTeamID(team.ID);
                team.Country = _cache.CountryByLeagueID(team.LeagueID);
                team.Region = _cache.RegionByRegionID(team.RegionID);
                team.Matches = _cache.MatchesByTeamId(team.ID);

                if (e.MyTeamId > 0 && team.ID == e.MyTeamId)
                    team.Economy = _cache.MyEconomy;

                if (team.Players != null)
                {
                    foreach (var player in team.Players)
                        player.Team = team;
                }

                if (team.Matches != null)
                {
                    foreach (var match in team.Matches)
                    {
                        match.HomeTeam = _cache.TeamById(match.HomeTeamId);
                        match.AwayTeam = _cache.TeamById(match.AwayTeamId);                        
                    }
                }
            }

            foreach (var league in _cache.LeagueUnits)
            {
                var leagueTeams = from team in _cache.Teams
                                  where team.LeagueLevelUnitID == league.ID
                                  select team;

                connectLeagueTeams(leagueTeams, league);
            }
        }

        private void connectLeagueTeams(IEnumerable<Team> leagueTeams, LeagueLevelUnit leagueUnit)
        {
            List<Team> teamsByPosition = new List<Team>();
            // fill positions
            for (int i = 0; i < 8; i++)
                teamsByPosition.Add(null);
            // replace with real teams
            foreach (var team in leagueTeams)
            {
                teamsByPosition[team.LeagueStats.Position - 1] = team;
                team.League = leagueUnit;
            }

            leagueUnit.TeamsByPosition = teamsByPosition.ToEntityCollection();
        }
   
        private void instanceEntites(FetchResultEventArgs e)
        {
            foreach (XDocument xml in e.Xmls)
            {
                if (xml == null)
                    continue;

                string fileName = xml.Root.Element("FileName").Value.ToLower();                 
                switch (fileName)
                {
                    case "teamdetails.xml":
                        instanceTeam(xml);
                        break;
                    case "players.xml":
                        instancePlayers(xml);
                        break;
                    case "worlddetails.xml":
                        instanceLeagueCountry(xml);
                        _countryWasUpdated = true;
                        break;
                    case "regiondetails.xml":
                        instanceRegion(xml);
                        break;
                    case "leaguedetails.xml":
                        instancePartialTeamLeagueStats(xml);
                        break;
                    case "leaguefixtures.xml":
                        instanceLeagueLevelUnit(xml);
                        instanceFullTeamLeagueStats(xml);
                        break;
                    case "economy.xml":
                        instanceEconomy(xml);
                        break;
                    case "matches.xml":
                        instanceMatches(xml);
                        break;
                    case "matchdetails.xml":
                        instanceMatchDeatils(xml);
                        break;
                    case "live.xml":
                        cacheLive(xml);
                        break;
                }
            }
        }

        private void instancePartialTeamLeagueStats(XDocument xml)
        {
            _cache.AddRange(TeamLeagueStats.FromLeagueDeatilsXml(xml));            
        }

        private void instanceFullTeamLeagueStats(XDocument xml)
        {
            //TODO: protection against null instances (for all instance functions)
            _cache.AddRange(TeamLeagueStats.FromLeagueFixturesXml(xml));            
        }

        private void instanceLeagueLevelUnit(XDocument xml)
        {
            _cache.Add(LeagueLevelUnit.FromLeaugeFixturesXml(xml));
        }

        private void instanceTeam(XDocument xml)
        {
            _cache.Add(Team.FromXml(xml));
        }

        private void instancePlayers(XDocument xml)
        {
            _cache.AddRange(Player.FromXml(xml));
        }

        private void instanceLeagueCountry(XDocument xml)
        {
            _cache.Add(LeagueCountry.FromWorldXml(xml));
        }

        private void instanceRegion(XDocument xml)
        {
            _cache.Add(Region.FromXml(xml));
        }

        private void instanceEconomy(XDocument xml)
        {
            _cache.MyEconomy = Economy.FromXml(xml);
        }

        private void instanceMatches(XDocument xml)
        {
            _cache.AddRange(Match.FromXml(xml));            
        }

        private void instanceMatchDeatils(XDocument xml)
        {
            _cache.Add(DetailedMatch.FromXml(xml));
        }

        private void cacheLive(XDocument xml)
        {
            // for some odd reason, viewNew is not working...
            _cache.UpdateLive(DetailedMatch.FromLiveXml(xml));
        }     
    }
}
